using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using DoubanFM.Model;
using DoubanFM.Util;

// Data model for DoubanFM music channels.

namespace DoubanFM.ViewModel
{
    /// <summary>
    /// Collection of groups and items for channels.
    /// </summary>
    public sealed class DataSource
    {
       // Re-use HttpClient.
        private readonly HttpClient _httpClient;
        // Singleton
        private static DataSource _instance = new DataSource();

        // Constrcutor
        private DataSource()
        {
            // Create a Http Client
            Utils.CreateHttpClient(ref _httpClient);
        }

        public static DataSource Instance
        {
            get { return _instance ?? (_instance = new DataSource()); }
        }

        #region Channel Related

        private const int ChannelFetchLimit = 20;
        private long _lastUpdateTime;

        public ChannelItemViewModel CurrentChannel { get; set; }

        private readonly ObservableCollection<ChannelGroupViewModel> _channelGroups = new ObservableCollection<ChannelGroupViewModel>();
        public ObservableCollection<ChannelGroupViewModel> ChannelGroups
        {
            get { return _channelGroups; }
        }

        public IEnumerable<ChannelGroupViewModel> GetGroups(string uniqueId)
        {
            if (!uniqueId.Equals("ChannelGroups")) throw new ArgumentException("Only 'ChannelGroups' is supported as a collection of groups");

            return ChannelGroups;
        }

        public ChannelGroupViewModel GetChannelGroup(string uniqueId)
        {
            // Simple linear search is acceptable for small data sets
            var matches = ChannelGroups.Where(group => group.UniqueId.Equals(uniqueId));
            var channelDataGroups = matches as List<ChannelGroupViewModel> ?? matches.ToList();
            return channelDataGroups.Any() ? channelDataGroups.First() : null;
        }

        public ChannelItemViewModel GetChannel(string uniqueId)
        {
            // Simple linear search is acceptable for small data sets
            var matches = ChannelGroups.SelectMany(group => group.Items).Where(item => item.UniqueId.Equals(uniqueId));
            var channelDataItems = matches as List<ChannelItemViewModel> ?? matches.ToList();
            return channelDataItems.Any() ? channelDataItems.First() : null;
        }

        public async Task NewChannelList(bool force)
        {
            // Skip if last update time less than 10 minutes
            var timeSpan = new TimeSpan(DateTime.Now.Ticks - _lastUpdateTime);
            if (!force && timeSpan.TotalMinutes <= 10)
            {
                return;
            }

            // Update time
            _lastUpdateTime = DateTime.Now.Ticks;

            // Clear old channels
            ClearChannelList();

            // Get Channels url list.
            var urlList = ChannelGroupSearch.PredefinedList(0, ChannelFetchLimit);
            // Create a query. 
            var downloadTasksQuery = from url in urlList select GetChannelsAsync(url);
            // Use ToArray to execute the query and start the download tasks.
            var downloadTasks = downloadTasksQuery.ToArray();
            // Await the completion of all the running tasks. 
            var statuses = await Task.WhenAll(downloadTasks);
            // Update Channels
            foreach (var channelStatus in statuses)
            {
                UpdateChannelList(channelStatus);
            }
        }

        private void ClearChannelList()
        {
            _channelGroups.Clear();
        }

        private void UpdateChannelList(ChannelResult result)
        {
            if (result == null)
            {
                return;
            }

            var group = new ChannelGroupViewModel(result.Search.GroupTitle, result.Search.GroupTitle, null, null, result.Search.GroupDescription);
            foreach (var channel in result.ChannelList.Channels)
            {
                group.Items.Add(new ChannelItemViewModel(channel, group));
            }
            _channelGroups.Add(group);
        }

        private async Task<ChannelResult> GetChannelsAsync(ChannelGroupSearch search)
        {
            ChannelResult result = null;

            try
            {
                // Get JSON result async
                var json = await _httpClient.GetStringAsync(search.SearchUrl);
                if (String.IsNullOrWhiteSpace(json))
                {
                    // TODO: Handle Exception
                }
                // Parse JSON result
                using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(json.ToCharArray())))
                {
                    var serializer = new DataContractJsonSerializer(typeof(ChannelResult));
                    result = (ChannelResult)serializer.ReadObject(ms);
                }
                // Bind search information
                result.Search = search;
            }
            catch (HttpRequestException)
            {
                // TODO: Handle Exception
            }

            return result;
        }    

        #endregion

        #region Music Related

        // Bindable Music List
        private readonly ObservableCollection<MusicItemViewModel> _musicList = new ObservableCollection<MusicItemViewModel>();
        public ObservableCollection<MusicItemViewModel> MusicList
        {
            get { return _musicList; }
        }

        // Bindable Music History List
        private readonly ObservableCollection<MusicItemViewModel> _musicHistoryList = new ObservableCollection<MusicItemViewModel>();
        public ObservableCollection<MusicItemViewModel> MusicHistoryList
        {
            get { return _musicHistoryList; }
        }

        private const int MusicHistoryLimit = 100;
        // Task for NewMusicList or MoreMusicList
        private Task _musicUpdateTask;

        /// <summary>
        /// Fetch the a new music list from Douban.FM
        /// This is wipe out the old music list
        /// </summary>
        /// <param name="channelId">Douban.FM Channel</param>
        /// <returns></returns>
        public async Task NewMusicList(string channelId)
        {
            // If another update task is running, simply return
            if (_musicUpdateTask != null && !_musicUpdateTask.IsCanceled && !_musicUpdateTask.IsCompleted && !_musicUpdateTask.IsFaulted)
            {
                return;
            }

            // Clear old musics
            ClearMusicList();

            // Get play list from douban.fm
            var task = GetMusicListAsync(new MusicSearch(MusicSearchType.New, "", channelId));
            _musicUpdateTask = task;

            var result = await task;
            _musicUpdateTask = null;

            // Update musics
            UpdateMusicList(result);
        }

        /// <summary>
        /// Fetch the a new music list from Douban.FM
        /// This is wipe out the old music list
        /// </summary>
        /// <param name="channelId">Douban.FM Channel</param>
        /// <param name="songId">Last Song ID </param>
        /// <returns></returns>
        public async Task MoreMusicList(string channelId, string songId)
        {
            // If another update task is running, simply return
            if (_musicUpdateTask != null && !_musicUpdateTask.IsCanceled && !_musicUpdateTask.IsCompleted && !_musicUpdateTask.IsFaulted)
            {
                return;
            }

            // Get play list from douban.fm
            var result = await GetMusicListAsync(new MusicSearch(MusicSearchType.More, songId, channelId));

            // Update musics
            UpdateMusicList(result);
        }

        /// <summary>
        /// Add a music to history
        /// Note: This do not remove the music from current list, you should take care of that you own
        /// </summary>
        /// <param name="music">Music to be added</param>
        public void AddToMusicHistory(MusicItemViewModel music)
        {
            // Try remove same music in history
            _musicHistoryList.Remove(music);

            // Make sure not exceed history limit
            while (_musicHistoryList.Count >= MusicHistoryLimit)
            {
                _musicHistoryList.RemoveAt(0);
            }

            // Add to history
            _musicHistoryList.Add(music);
        }

        #region Private methods

        private async Task<MusicResult> GetMusicListAsync(MusicSearch search)
        {
            MusicResult result = null;

            try
            {
                // Get JSON result async
                var json = await _httpClient.GetStringAsync(search.SearchUrl);
                if (String.IsNullOrWhiteSpace(json))
                {
                    // TODO: Handle Exception
                }
                else
                {
                    // Parse JSON result
                    using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(json.ToCharArray())))
                    {
                        var serializer = new DataContractJsonSerializer(typeof(MusicResult));
                        result = (MusicResult)serializer.ReadObject(ms);
                    }
                }
            }
            catch (HttpRequestException)
            {
                // TODO: Handle Exception
            }

            return result;
        }

        private void UpdateMusicList(MusicResult result)
        {
            if (result == null)
            {
                return;
            }

            foreach (var music in result.MusicList)
            {
                var item = new MusicItemViewModel(music);
                // Skip if already in the music list
                if (_musicList.Contains(item)) continue;
                // Skip if already in the history, unless history reach 3/4 channel music count
                if (_musicHistoryList.Count > CurrentChannel.SongNum * 0.75 || !_musicHistoryList.Contains(item))
                {
                    _musicList.Add(item);
                }
            }
        }

        private void ClearMusicList()
        {
            _musicList.Clear();
        }

        #endregion

        #endregion
    }
}
